/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.transaction.jdbc;

import cn.easyplatform.dao.utils.DaoUtils;
import cn.easyplatform.lang.Lang;
import cn.easyplatform.transaction.Atom;
import cn.easyplatform.transaction.ConnectionInfo;
import cn.easyplatform.transaction.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;


/**
 * 用模板的方式操作事务
 *
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a>
 */
public abstract class JdbcTransactions {

    private static final Logger log = LoggerFactory.getLogger(JdbcTransactions.class);

    private static final ThreadLocal<List<ConnectionInfo>> resources = new ThreadLocal<List<ConnectionInfo>>();

    private final static ThreadLocal<Transaction> trans = new ThreadLocal<Transaction>();

    private final static ThreadLocal<Integer> count = new ThreadLocal<Integer>();

    /**
     * @param level
     * @throws Exception
     */
    private static void _begain(int level) throws SQLException {
        Transaction tn = trans.get();
        if (null == tn) {
            tn = new JdbcTransaction();
            tn.setLevel(level);
            trans.set(tn);
            count.set(0);
            if (log.isDebugEnabled())
                log.debug("Start New Transaction id={}, level={}", tn.getId(),
                        level);
        } else {
            if (log.isDebugEnabled())
                log.debug("Attach Transaction    id={}, level={}", tn.getId(),
                        level);
        }
        int tCount = count.get() + 1;
        count.set(tCount);

    }

    /**
     * @throws SQLException
     */
    private static void _commit() throws SQLException {
        count.set(count.get() - 1);
        Transaction tn = trans.get();
        if (count.get() == 0) {
            if (log.isDebugEnabled())
                log.debug("Transaction Commit id=" + tn.getId());
            tn.commit();
        } else {
            if (log.isDebugEnabled())
                log.debug("Transaction delay Commit id={}, count={}", tn.getId(),
                        count.get());
        }
    }

    public static final Transaction get() {
        return trans.get();
    }

    /**
     *
     */
    private static void _depose() {
        if (count.get() != null && count.get() == 0 && trans.get() != null) {
            try {
                if (log.isDebugEnabled())
                    log.debug("Transaction depose id={}, count={}", trans.get()
                            .getId(), count.get());
                trans.get().close();
            } finally {
                trans.set(null);
                count.set(null);
            }
        }
    }

    /**
     * @param num
     */
    private static void _rollback(Integer num) {
        count.set(num);
        if (trans.get() != null) {
            if (count.get() == 0) {
                if (log.isDebugEnabled())
                    log.debug("Transaction rollback id={}, count={}", trans.get()
                            .getId(), num);
                trans.get().rollback();
            } else {
                if (log.isDebugEnabled())
                    log.debug("Transaction delay rollback id={}, count={}", trans
                            .get().getId(), num);
            }
        }
    }

    /**
     * 执行一组原子操作，默认的事务级别为: TRANSACTION_READ_COMMITTED。详细请看 exec(int level,
     * Atom... atoms) 函数的说明
     *
     * @param atoms 原子操作对象
     */
    public static void exec(Atom... atoms) {
        exec(Connection.TRANSACTION_READ_COMMITTED, atoms);
    }

    /**
     * 执行一组原子操作，并指定事务级别。
     * <p>
     * 这里需要注意的是，easyplatform 支持事务模板的无限层级嵌套。
     * 这里，如果每一层嵌套，指定的事务级别有所不同，不同的数据库，可能引发不可预知的错误。
     * <p>
     * 所以，嵌套的事务模板的事务，将以最顶层的事务为级别为标准。就是说，如果最顶层的事务级别为
     * 'TRANSACTION_READ_COMMITTED'，那么下面所包含的所有事务，无论你指定什么样的事务级别，都是
     * 'TRANSACTION_READ_COMMITTED'， 这一点，由抽象类 Transaction 来保证。其 setLevel
     * 当被设置了一个大于 0 的整数以后，将不再 接受任何其他的值。
     * <p>
     * 你可以通过继承 Transaction 来修改这个默认的行为，当然，这个行为修改一般是没有必要的。
     * <p>
     * 另外，你还可能需要知道，通过 Transactions.setup 方法，能让整个虚拟机的 easyplatform 事务操作都使用你的
     * Transaction 实现
     *
     * @param level 事务的级别。
     *              <p>
     *              你可以设置的事务级别是：
     *              <ul>
     *              <li>java.sql.Connection.TRANSACTION_NONE
     *              <li>java.sql.Connection.TRANSACTION_READ_UNCOMMITTED
     *              <li>java.sql.Connection.TRANSACTION_READ_COMMITTED
     *              <li>java.sql.Connection.TRANSACTION_REPEATABLE_READ
     *              <li>java.sql.Connection.TRANSACTION_SERIALIZABLE
     *              </ul>
     *              不同的数据库，对于 JDBC 事务级别的规范，支持的力度不同。请参看相应数据库的文档，已
     *              确定你设置的数据库事务级别是否被支持。
     * @param atoms 原子操作对象
     * @see cn.easyplatform.transaction.Transaction
     * @see java.sql.Connection
     */
    public static void exec(int level, Atom... atoms) {
        if (null == atoms)
            return;
        int num = count.get() == null ? 0 : count.get();
        try {
            _begain(level);
            for (Atom atom : atoms)
                atom.run();
            _commit();
        } catch (Throwable e) {
            _rollback(num);
            throw Lang.wrapThrow(e);
        } finally {
            _depose();
        }
    }

    /* ===========================下面暴露几个方法给喜欢 try...catch...finally 的人 ===== */

    /**
     * 开始一个事务，级别为 TRANSACTION_READ_COMMITTED
     * <p>
     * 你需要手工用 try...catch...finally 来保证你提交和关闭这个事务
     *
     * @throws Exception
     */
    public static void begin() throws SQLException {
        _begain(Connection.TRANSACTION_READ_COMMITTED);
    }

    /**
     * 开始一个指定事务
     * <p>
     * 你需要手工用 try...catch...finally 来保证你提交和关闭这个事务
     *
     * @param level 指定级别
     * @throws Exception
     */
    public static void begin(int level) throws SQLException {
        _begain(level);
    }

    /**
     * 提交事务，执行它前，你必需保证你已经手工开始了一个事务
     *
     * @throws Exception
     */
    public static void commit() throws SQLException {
        _commit();
    }

    /**
     * 回滚事务，执行它前，你必需保证你已经手工开始了一个事务
     *
     * @throws Exception
     */
    public static void rollback() {
        Integer c = count.get();
        if (c == null)
            c = Integer.valueOf(0);
        else if (c > 0)
            c--;
        _rollback(c);
    }

    /**
     * 关闭事务，执行它前，你必需保证你已经手工开始了一个事务
     *
     * @throws Exception
     */
    public static void close() {
        _depose();
    }

    /**
     * 如果在事务中,则返回事务的连接,否则直接从数据源取一个新的连接
     */
    public static Connection getConnection(DataSource ds) throws SQLException {
        if (trans.get() == null) {
            if (resources.get() == null)
                resources.set(new ArrayList<ConnectionInfo>());
            for (ConnectionInfo p : resources.get())
                if (p.getDs() == ds)
                    return p.getConn();
            Connection conn = ds.getConnection();
            resources.get().add(new ConnectionInfo(ds, conn));
            return conn;
        } else
            return trans.get().getConnection(ds);
    }

    /**
     * 清除正常的连接以及未及时关闭的事务连接
     */
    public static void clear() {
        if (trans.get() != null) {
            try {
//                if (log.isDebugEnabled())
//                    log.debug("Transaction depose id={}, count={}", trans.get()
//                            .getId(), count.get());
                trans.get().close();
            } finally {
                trans.remove();
                count.remove();
            }
        }
        if (resources.get() == null)
            return;
        for (ConnectionInfo p : resources.get())
            DaoUtils.closeQuietly(p.getConn());
        resources.get().clear();
        resources.remove();
    }
}
